package br.org.mestrado.vais;

import indicadores.Indicador;
import indicadores.Spread;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

public class VAIS {

	static final int NUM_OBJECTIVES = Integer.parseInt(System
			.getProperty("objectives"));

	static final int NUM_DECISION_VAR = Integer.parseInt(System
			.getProperty("variables"));

	// controle de qual objetivo deve é minimização
	static final String[] control = new String[NUM_OBJECTIVES];

	// critério de parada é o nº de fitness call
	// ou número de gerações
	static final int STOP_CRTERIA = Integer.parseInt(System
			.getProperty("stopCriteria"));

	// total de inner loops
	static final int INNER_LOOPS = Integer.parseInt(System
			.getProperty("innerLoops"));

	// contador de runs
	static final int RUNS = Integer.parseInt(System.getProperty("runs"));

	// contador específico para problemas do japa doidão cosseno
	static double START_PROB = Double.parseDouble(System.getProperty("sProb"));

	// contador específico para problemas do japa doidão cosseno
	static double END_PROB = Double.parseDouble(System.getProperty("eProb"));

	// população inicial e máxima
	static final int N_POP = Integer.parseInt(System.getProperty("nPop"));

	// tamanho nova população inicial e máxima
	static final int N_NEW_POP = (int) (N_POP * 0.2);

	// máximo de Ag na memória
	static final int N_MAX_MEN = Integer
			.parseInt(System.getProperty("nMaxMem"));

	// total de clones por Ag
	static final int N_CLONES = Integer.parseInt(System.getProperty("nClones"));

	// problema escolhido
	static final PROBLEMA problema = PROBLEMA.valueOf(System
			.getProperty("problema"));

	// maracador mínimo de fitness
	static double MIN_FITNESS = Double.MIN_VALUE;

	// marcador máximo de fitness
	static double MAX_FITNESS = Double.MIN_VALUE;

	// incrementador único de ID de clone
	static AtomicInteger atomicInteger = new AtomicInteger(N_POP);

	// tipo de avaliação pareto a ser instanciada
	static final ParetoMin pareto = new ParetoMin(NUM_OBJECTIVES);

	static final String STORE_PATH = System.getProperty("user.home")
			+ "/VIS_many_obj/" + problema.name() + "/" + NUM_OBJECTIVES + "/";

	// precisão do cálculo
	static final MathContext MATH_CONTEXT = new MathContext(20,
			RoundingMode.HALF_EVEN);

	static final DecimalFormat ddf = new DecimalFormat("0.00",
			new DecimalFormatSymbols(Locale.US));

	// limiar de supressão
	static final double thresholdSupression = Math.sqrt(NUM_OBJECTIVES) / N_POP;

	// logger
	private static Logger logger = Logger.getLogger(VAIS.class.getPackage()
			.getName());

	static List<Anticorpo> generatePopulation(double porcentage) {

		final List<Anticorpo> pop = new ArrayList<Anticorpo>();

		do {

			for (int i = 0; i < porcentage; i++) {

				pop.add(new Anticorpo(i, NUM_OBJECTIVES, NUM_DECISION_VAR,
						N_CLONES, N_CLONES, 0.06, problema, atomicInteger));

			}

			for (Anticorpo anticorpo : pop) {
				// calcula os objetivos
				anticorpo.calculateObjectives(true);
			}

			for (Iterator<Anticorpo> iterator = pop.iterator(); iterator
					.hasNext();) {
				Anticorpo anticorpo = iterator.next();
				if (!problema.isConstrained(anticorpo))
					iterator.remove();
			}

		} while (pop.size() < porcentage);

		return pop;
	}

	static void masterReset(final List<Anticorpo> pop) {

		MAX_FITNESS = Double.MIN_VALUE;

		MIN_FITNESS = Double.MAX_VALUE;

		// reseta população.
		// próxima iteração terá os valores atualizados

		// ajusta relações
		// verifica população de clones/inicial com a população atual
		// para avaliaçãode fitness
		for (Anticorpo anticorpoAComparar : pop) {

			// zera controles de fitness
			anticorpoAComparar.resetControls();

			// avalia reçãode dominância
			relationEvaluate(anticorpoAComparar, pop);

			// marca o fitness mais alto
			if (MAX_FITNESS < anticorpoAComparar.getFitnessValue()) {
				MAX_FITNESS = anticorpoAComparar.getFitnessValue();
			}
			if (MIN_FITNESS > anticorpoAComparar.getFitnessValue()) {
				MIN_FITNESS = anticorpoAComparar.getFitnessValue();
			}
		}

		for (Anticorpo anticorpo : pop) {
			anticorpo.setControls(MAX_FITNESS, MIN_FITNESS);
		}

	}

	static void cleanMemory(final ArrayList<Anticorpo> memory) {

		// remove os null

		for (Iterator<Anticorpo> iterator = memory.iterator(); iterator
				.hasNext();) {
			Anticorpo anticorpo = iterator.next();

			if (anticorpo == null)
				iterator.remove();

		}
	}

	static void checkMemory(final List<Anticorpo> newAg,
			final ArrayList<Anticorpo> memory) {

		for (Anticorpo anticorpo : newAg) {

			// caso memória tenha indivíduos, realiza um pré clean
			// na memória, mesmo q clone seja um dominado
			// caso o número de clones que chegaram a esse ponto seja menor
			// que o mnáximo de clones, pela regra da transitividade
			// os itens da memória dominadados por um cloneA1 que é dominado
			// por um cloneA2 também é serão dominados por cloneA2.
			// para evitar conparações desnecessárias, verifica o ID do clone
			// caso nãop estaja dentro, clean-up
			if (anticorpo.getFitnessValue() == 0 && !memory.contains(anticorpo)) {

				for (Iterator<Anticorpo> iterator = memory.iterator(); iterator
						.hasNext();) {

					Anticorpo memAnticorpo = iterator.next();

					// remove caso domine algum da memória
					if (LEGENDA.DOMINA.valor() == pareto.compare(anticorpo,
							memAnticorpo)) {
						logger
								.finest("Item de memoria dominado. Removendo item: "
										+ memAnticorpo.getID());

						iterator.remove();
					}
				}

				// verifica se o tamanho não é 100

				if (memory.size() == N_MAX_MEN) {

					logger.finest("Memoria full. Removendo mais antigo");

					memory.remove((int) Math.floor(N_MAX_MEN * Math.random()));
				}

				// adiciona o clone na memória
				try {
					memory.add((Anticorpo) anticorpo.clone());
				} catch (CloneNotSupportedException e) {
					e.printStackTrace();
				}

			}
		}

	}

	private static void resetOutPut(final String path) {

		final File dir_base = new File(path);

		if (dir_base.exists()) {

			// zera todos os diretórios
			for (final File dir : dir_base.listFiles()) {
				dir.delete();
			}

			dir_base.delete();

		}

		if (!dir_base.exists())
			dir_base.mkdirs();

	}

	private static void writeOutPut(final String path,
			final ArrayList<Anticorpo> pop) {

		try {

			final File dir = new File(path);

			if (!dir.exists())
				dir.mkdir();

			final PrintWriter writer1 = new PrintWriter(new FileWriter(
					new File(dir, "/run_objective.fit"), true));

			final PrintWriter writer2 = new PrintWriter(new FileWriter(
					new File(dir, "/run_variables.fit"), true));

			for (Anticorpo anticorpo : pop) {

				writer1.println(anticorpo.toString());
				writer2.println(Arrays.toString(anticorpo
						.getDecisionVariables()));

			}
			writer1.println();
			writer2.println();

			writer1.close();
			writer2.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	public static void main(final String... args) {

		Thread thrd = Thread.currentThread();

		logger.getParent().setLevel(Level.FINE);

		// ajusta max fitness
		problema.setMaxFitnessCount(STOP_CRTERIA);

		do {

			final BigDecimal bd_prob = new BigDecimal(START_PROB,
					new MathContext(2, RoundingMode.HALF_EVEN));

			thrd.setName("Main Thread Name: Total objectives = "
					+ NUM_OBJECTIVES + " | Prob = " + bd_prob.doubleValue());

			logger.info(Thread.currentThread().getName());

			final String prob_n = ddf.format(bd_prob.doubleValue());

			// caminho onde vai ser armazenado os arquivos
			final String path = STORE_PATH + "/" + prob_n;

			// limpa diretórios
			resetOutPut(path);

			Arrays.fill(control, "-");

			// ajusta probabilidade para esta roda
			pareto.setS(bd_prob.doubleValue());

			// contador de runs do problema
			int runsCount = 0;

			do {

				final ArrayList<Anticorpo> pop = new ArrayList<Anticorpo>();

				final ArrayList<Anticorpo> memory = new ArrayList<Anticorpo>();

				logger.info("Start Running: " + runsCount);

				outerLoop(pop, memory);

				logger.info("Run Stop 			-  Sumary");
				logger.info("Pop size			: " + pop.size());
				logger
						.info("Total finess call	: "
								+ problema.fitnessCout.get());

				// incrementa contadores de runs
				runsCount++;

				// escreve saída
				writeOutPut(path, pop);

			} while (runsCount < RUNS);

			// calcula spread
			Indicador indicador = new Spread(NUM_OBJECTIVES, path, "");

			indicador.preencherObjetivosMaxMin(control);

			try {
				indicador.calcularIndicadorArquivo(path + "/run_objective.fit");
			} catch (IOException e) {
				System.err.println(e);
			}
			START_PROB += 0.05d;
		} while (START_PROB <= END_PROB);

	}

	static void outerLoop(final ArrayList<Anticorpo> pop,
			final ArrayList<Anticorpo> memory) {

		pop.addAll(generatePopulation(N_POP));

		// zera contador de problemas
		problema.fitnessCout.set(0);

		final ArrayList<Anticorpo> ref = new ArrayList<Anticorpo>(1);

		ref.add(null);

		masterReset(pop);

		// loop principal da operação
		do {

			logger.fine("Total Fitness Call:" + problema.fitnessCout.get());

			// loop para a parte interna do algoritmo, clonagem e mutação
			for (int counterIn = 0; counterIn < INNER_LOOPS
					&& !problema.stopNow(); counterIn++) {

				innerLoop(memory, pop);

				logger.finer("Inner Loop. Counter    : " + counterIn);
				logger.finer("Inner Loop. Memory size: " + memory.size());

			}

			// aplica operadores de afinidade e supressão
			reduce(memory);

			cleanMemory(memory);
			// substitui população
			pop.clear();

			// adiciona a memória na população
			pop.addAll(memory);

			logger.fine("Memory reduce size: " + memory.size());

			// adiciona novos indivíduos
			if (!problema.stopNow()) {

				logger.fine("Add random pop. Size: " + N_NEW_POP);

				pop.addAll(generatePopulation(N_NEW_POP));

				memory.clear();

				// reset
				masterReset(pop);

			}

		} while (!problema.stopNow());

	}

	static void innerLoop(final ArrayList<Anticorpo> memory,
			final List<Anticorpo> pop) {

		// recupera iterator da população
		for (ListIterator<Anticorpo> popIterator = pop.listIterator(); popIterator
				.hasNext()
				&& !problema.stopNow();) {

			final List<Anticorpo> popClone = new ArrayList<Anticorpo>();

			// pega indivíduo
			Anticorpo anticorpo = popIterator.next();

			// gera clones
			popClone.addAll(anticorpo.getEvaluatedClones());

			// avalia fitness de todos os clones
			// entre os clones
			relationsEvaluate(popClone, popClone);

			// avaliação fitness do pai com os clones
			relationEvaluate(anticorpo, popClone);

			// avaliação o fitnesse dos clones entre a população
			relationsEvaluate(popClone, pop);

			// verifica qual celula vai para a próxima geração
			chooseClone(popIterator, anticorpo, popClone);

			popClone.add(anticorpo);

			// verifica memória contra a população de clones
			checkMemory(popClone, memory);

			// reseta total de dominados
			masterReset(pop);

		}
	}

	static void chooseClone(final ListIterator<Anticorpo> popIterator,
			final Anticorpo parent, final List<Anticorpo> anticorpos) {

		// verifica se o pai e pior qe os clones em relaçao ao fitness
		for (Iterator<Anticorpo> proleIterator = anticorpos.iterator(); proleIterator
				.hasNext();) {
			Anticorpo anticorpoProle = proleIterator.next();

			final int resultCopare = parent.compareTo(anticorpoProle);

			// se a prole for pior q o pai, remove da collection
			if (resultCopare == -1)
				proleIterator.remove();

		}

		if (!anticorpos.isEmpty()) {

			// se tiver mais de uma cara
			// random e pega o primeiro
			if (anticorpos.size() > 1) {
				Collections.shuffle(anticorpos);

				Collections.sort(anticorpos);
			}

			popIterator.set(anticorpos.get(0));
		}

		// ajusta omega de todos os envolvidos
		parent.calculateOmega(anticorpos);

	}

	static void relationsEvaluate(final List<Anticorpo> pop,
			final List<Anticorpo> popReferencia) {

		// verifica população de clones/inicial com a população atual
		// para avaliaçãode fitness
		for (final Iterator<Anticorpo> iteratorPop = pop.iterator(); iteratorPop
				.hasNext();) {

			// pega o iterador da população 1 e compara com o resto da
			// população
			final Anticorpo anticorpoAComparar = iteratorPop.next();

			relationEvaluate(anticorpoAComparar, popReferencia);

		}

	}

	static void relationEvaluate(final Anticorpo anticorpo,
			final List<Anticorpo> popReferencia) {

		for (int pointer = 0; pointer < popReferencia.size(); pointer++) {
			// pega iterador da população para comparação
			final Anticorpo anticorpoReferencia = popReferencia.get(pointer);

			// resultado da avaliação
			final int resultPareto = pareto.compare(anticorpo,
					anticorpoReferencia);

			if (resultPareto == LEGENDA.DOMINADO.valor()) {

				anticorpo.addFitness();

			}
		}

	}

	static void reduce(final ArrayList<Anticorpo> memory) {

		// realiza o cálculo de afinidade na mesma medida em que vai
		// suprimindo as células da memória

		// baseado na distância euclidiana entre todo os indivíduos
		supress: for (int pointer = 0; pointer < memory.size(); pointer++) {

			// verifica se o ponteiro não aponta um cara marcado para morrer
			if (memory.get(pointer) == null)
				continue supress;

			// caso não esteja na lista, toca o barco
			Anticorpo anticorpo = memory.get(pointer);

			// recupera valores objetivos
			double[] tmp = anticorpo.getObjectives();

			vizinhos: for (int inner_pointer = 0; inner_pointer < memory.size(); inner_pointer++) {

				// veifica se não é ele mesmo
				if (pointer == inner_pointer
						|| memory.get(inner_pointer) == null)
					continue vizinhos;

				Anticorpo AB = memory.get(inner_pointer);

				double[] inner_tmp = AB.getObjectives();

				double dist_euclid = 0d;

				for (int i = 0; i < inner_tmp.length; i++) {

					dist_euclid += Math.pow(tmp[i] - inner_tmp[i], 2);

				}

				dist_euclid = Math.sqrt(dist_euclid);

				if (dist_euclid < thresholdSupression) {

					// marca o cara como null
					memory.set(inner_pointer, null);

				}
			}
		}
	}
}
